package org.alfresco.crowd;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import net.sf.acegisecurity.GrantedAuthority;
import net.sf.acegisecurity.GrantedAuthorityImpl;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.dao.User;
import net.sf.acegisecurity.providers.encoding.PasswordEncoder;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationComponentImpl;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.NTLMMode;
import org.alfresco.repo.security.authentication.RepositoryAuthenticationDao;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.atlassian.crowd.integration.acegi.user.CrowdUserDetails;
import com.atlassian.crowd.integration.acegi.user.CrowdUserDetailsService;

public class CrowdAlfrescoAuthenticationComponent extends
		AuthenticationComponentImpl {

	private CrowdUserDetailsService userDetailsService;

	private AuthorityService authorityService;

	private RepositoryAuthenticationDao authenticationDao;

	private PasswordEncoder passwordEncoder;

	private NodeService nodeService;

	private PersonService personService;

	private TransactionService transactionService;

	// Careful. Alfresco might eventually start using a salt
	private String salt = null;

	private static Log logger = LogFactory
			.getLog(CrowdAlfrescoAuthenticationComponent.class);

	private Map<QName, Serializable> createDefaultProperties(String userName,
			String firstName, String lastName, String email, String orgId,
			NodeRef home) {
		HashMap<QName, Serializable> properties = new HashMap<QName, Serializable>();
		properties.put(ContentModel.PROP_USERNAME, userName);
		properties.put(ContentModel.PROP_HOMEFOLDER, home);
		properties.put(ContentModel.PROP_FIRSTNAME, firstName);
		properties.put(ContentModel.PROP_LASTNAME, lastName);
		properties.put(ContentModel.PROP_EMAIL, email);
		properties.put(ContentModel.PROP_ORGID, orgId);
		return properties;
	}

	public String encodePassword(String string) {
		return passwordEncoder.encodePassword(string, salt);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.alfresco.repo.security.authentication.AuthenticationComponentImpl#authenticateImpl(java.lang.String,
	 *      char[])
	 */
	@Override
	protected void authenticateImpl(String userName, char[] password)
			throws AuthenticationException {

		logger.info("Called authenticateImpl");
		
		// We want our system to be case-insensitive
		userName = userName.toLowerCase();

		// This will implicitly call getUserDetails
		super.authenticateImpl(userName, password);

		// Also add the user to the alfrescoUserStore if they don't exist
		NodeRef userRef = authenticationDao.getUserOrNull(userName);
		if (userRef == null) {
			logger.debug("Calling createUser: " + userName);
			authenticationDao.createUser(userName, password);
		} else
			logger.debug("User exists in alfrescoUserStore: " + userName);

		// The user needs to have either username or password the same in Crowd
		// and Alfresco
		// or else we don't know its the same user.
		if (!userName.equalsIgnoreCase("admin")
				&& !userName.equalsIgnoreCase("guest")) {
			CrowdUserDetails crowdUd = userDetailsService
					.loadUserByUsername(userName);
			NodeRef person = getPersonService().getPerson(userName);
			String email1 = crowdUd.getEmail().toLowerCase();
			String email2 = (DefaultTypeConverter.INSTANCE.convert(
					String.class, this.getNodeService().getProperty(person,
							ContentModel.PROP_EMAIL))).toLowerCase();

			String password1 = authenticationDao.getMD4HashedPassword(userName);
			String password2 = encodePassword(new String(password));
			boolean passwordsOutOfSync = password1 == null || password2 == null
					|| !password1.equals(password2);
			if (passwordsOutOfSync)
				logger.debug("Passwords out of sync");

			String emails = "Crowd directory is " + email1
					+ " and Alfresco is " + email2 + ". ";
			logger.debug(emails);
			boolean emailsOutOfSync = email1 == null || email2 == null
					|| !email1.equals(email2);
			if (emailsOutOfSync)
				logger.debug("Emails out of sync");

			if (emailsOutOfSync && passwordsOutOfSync) {
				String error = "Email addresses differ between global directory and Alfresco. "
						+ emails
						+ "Contact the Alfresco administrator and ask them to correct your email address.";
				logger.error(error);
				throw new AuthenticationBlockedException(error);
			}

			if (passwordsOutOfSync) {
				logger
						.warn("Passwords differ. This should only happen once after a password change. User "
								+ userName);
				// Reset password to Crowd password
				// We need to copy the users password into the DB so its
				// available for checking
				authenticationDao.updateUser(userName, password);
			}

			if (emailsOutOfSync) {
				// logger.debug("Calling syncPersonProperties");

				// syncPersonProperties Disabled for now.. Problem with writing
				// to repos on login
				// Caused by: org.alfresco.error.AlfrescoRuntimeException:
				// Update failed: protected
				// property 'sizeCurrent'

				// syncPersonProperties(userName, crowdUd, person);
			}

		}

	}

	private void syncPersonProperties(String userName,
			CrowdUserDetails crowdUd, NodeRef person) {

		String email = (DefaultTypeConverter.INSTANCE.convert(String.class,
				this.getNodeService().getProperty(person,
						ContentModel.PROP_EMAIL))).toLowerCase();
		String first = (DefaultTypeConverter.INSTANCE.convert(String.class,
				this.getNodeService().getProperty(person,
						ContentModel.PROP_FIRSTNAME)));
		String last = (DefaultTypeConverter.INSTANCE.convert(String.class, this
				.getNodeService().getProperty(person,
						ContentModel.PROP_LASTNAME)));

		// Update name and email if needed
		HashMap<QName, Serializable> properties;
		{
			properties = new HashMap<QName, Serializable>();
			if (!(crowdUd.getEmail().toLowerCase().equals(email)))
				properties.put(ContentModel.PROP_EMAIL, crowdUd.getEmail()
						.toLowerCase());
			if (!(crowdUd.getFirstName().equals(first)))
				properties.put(ContentModel.PROP_FIRSTNAME, crowdUd
						.getFirstName());
			if (!(crowdUd.getLastName().equals(last)))
				properties.put(ContentModel.PROP_LASTNAME, crowdUd
						.getLastName());
		}
		if (!properties.isEmpty()) {
			logger.debug("Updating person name or email from Crowd");
			personService.setPersonProperties(userName, properties);
		}

		// Update authorities if needed

	}

	/**
	 * Default implementation that makes an ACEGI object on the fly
	 * 
	 * @param userName
	 * @return
	 */
	@Override
	public UserDetails getUserDetails(String userName) {

		// We want our system to be case-insensitive
		userName = userName.toLowerCase();
		
		// User may not exist yet if they have never logged in to Alfresco
		// before
		PersonService personService = this.getPersonService();
		NodeRef out = null;
		logger.debug("getUserDetails for:" + userName);
		if (!personService.personExists(userName)) {

			// Get user details
			CrowdUserDetails crowdUd = userDetailsService
					.loadUserByUsername(userName);
			logger.debug("Fetched user from Crowd:" + crowdUd.getEmail());

			out = this.getPersonService().createPerson(
					createDefaultProperties(userName, crowdUd.getFirstName(),
							crowdUd.getLastName(), crowdUd.getEmail(), null,
							null));
			logger.debug("Created new person: " + out.toString());
		} else
			logger.debug("Person exists");

		GrantedAuthority[] gas = new GrantedAuthority[1];
		gas[0] = new GrantedAuthorityImpl("ROLE_AUTHENTICATED");
		UserDetails ud = new User(userName, "", true, true, true, true, gas);
		return ud;

	}

	/**
	 * Get the enum that describes NTLM integration
	 * 
	 * @return NTLMMode
	 */
	public NTLMMode getNTLMMode() {
		return NTLMMode.PASS_THROUGH;
	}

	/**
	 * Get the password hash from the DAO
	 */
	public String getMD4HashedPassword(String userName) {
		logger.warn("Getting MD4 password. It may not be set correctly");
		return super.getMD4HashedPassword(userName);
	}

	/**
	 * IOC
	 * 
	 * @param authenticationDao
	 */
	public void setAuthenticationDao(
			RepositoryAuthenticationDao authenticationDao) {
		this.authenticationDao = authenticationDao;
	}

	public void setUserDetailsService(CrowdUserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	public void setAuthorityService(AuthorityService authorityService) {
		this.authorityService = authorityService;
	}

	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		this.passwordEncoder = passwordEncoder;
	}

	public void setNodeService(NodeService nodeService) {
		this.nodeService = nodeService;
	}

	public void setPersonService(PersonService personService) {
		this.personService = personService;
	}

	public void setTransactionService(TransactionService transactionService) {
		this.transactionService = transactionService;
	}

	public CrowdUserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

	public AuthorityService getAuthorityService() {
		return authorityService;
	}

	public NodeService getNodeService() {
		return nodeService;
	}

	public PersonService getPersonService() {
		return personService;
	}

	public TransactionService getTransactionService() {
		return transactionService;
	}

}
